跳到主要内容

Go 语言学习-“基础语法”

Go 程序组成

package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

package main 表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。package main 包下可以有多个文件,但所有文件中只能有一个 main() 方法,main() 方法代表程序入口。

下一行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。

下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);

标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

需要注意的是 { 不能单独放在一行,所以以下代码在运行时会产生错误:

package main

import "fmt"

func main()
{ // 错误,{ 不能在单独的行上
fmt.Println("Hello, World!")
}

入口函数和导包

整个 Go 程序的执行流程

执行示例:

main

import (
"std01/lib1"
"std01/lib2"
)

func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}

lib1(lib2 同理)

package lib1

import "fmt"

func Lib1Test() {
fmt.Println("this is Lib1 Function")
}

func init() {
fmt.Println("this is lib1")
}

执行后打印,可以看到是 init 先执行

匿名导包和别名

在 Go 中导包不使用里面的函数,会报错,如何只导包不使用它的方法呢?(导包还是会执行 init 方法)

只需在前面加一个 _ 就行了

import (
_ "std01/lib1"
"std01/lib2"
)

func main() {
lib2.Lib2Test()
}

也可以给包使用别名

import (
ali "std01/lib2"
)

func main() {
ali.Lib2Test()
}

如果不想写包名,直接使用里面的方法可以使用 . 类似 Java 的静态导入

import (
. "std01/lib2"
)

func main() {
Lib2Test()
}

但是尽量少用这种方式导包,因为当两个包都存在同一个名称的方法就会产生歧义

变量的声明方式

声明变量是靠 var 关键字,这种方式也会分配内存,不会像 Java 那样只给一个指针

声明变量的一般形式是使用 var 关键字:

var identifier type

可以一次创建多个变量

var identifier1, identifier2 type

而且类型可以不同

var ag1,ag2 = 100, "abc"

多行多变量声明

var (
ag1 int = 100
ag2 bool = true
)

实例:

package main
import "fmt"
func main() {
var a string = "Test"
fmt.Println(a)

var b, c int = 1, 2
fmt.Println(b, c)
}

如果没有初始化,则变量默认为零值。

  • 数值类型(包括complex64/128)为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)
  • 以下几种类型为 nil:
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口

可以使用 := 来简写初始化变量

f := "Runoob" 
// 等价于
var f string = "Runoob"

intVal := 1 等价于:

var intVal int 
intVal =1

所以下面这样就重复创建了

var intVal int 
intVal :=1 // 这时候会产生编译错误,因为 intVal 已经声明,不需要重新声明

但是注意,:= 无法声明全局变量,它只能用在函数体内

定义常量与枚举 iota

const length int = 10

Go 语言定义枚举也是通过常量来实现的

const (
MEN = 0
WOMEN = 1
UNKOWN = 2
)

可以使用 iota 关键字省略这个数字,下面这个等价于上面的

const (
MEN = iota // 0
WOMEN // 1
UNKOWN // 2
)

注意:iota 只能用在 const 框中

而且可以在这个 iota 上设置一个等式

const (
MEN = 10* iota // 0
WOMEN // 10
UNKOWN // 20
)

可以使用多个 iota

const (
// iota = 0, a = iota + 1,b = iota + 2, a = 1, b = 2
a, b = iota + 1, iota + 2
// iota = 1, c = iota + 1,d = iota + 2, c = 2, b = 3
c, d
// iota = 2, e = iota + 1,f = iota + 2, e = 3, f = 4
e, f

// iota = 3, g = iota * 2,h = iota * 3, g = 6, h = 9
g, h = iota * 2, iota * 3
// iota = 4, i = iota * 2,k = iota * 3, i = 8, k = 12
i, k
)

可以看到 iota 主要就是用于定义一个自增的规则

定义跳转标签

for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母

func main() {
LABEL1:
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if j == 4 {
continue LABEL1
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}

continue 语句指向 LABEL1,当执行到该语句的时候,就会跳转到 LABEL1 标签的位置。

可以看到当 j=4 和 j=5 的时候,没有任何输出:标签的作用对象为外部循环,因此 i 会直接变成下一个循环的值,而此时 j 的值就被重设为 0,即它的初始值。如果将 continue 改为 break,则不会只退出内层循环,而是直接退出外层循环了。

值类型和引用类型 ⭐

值类型分别有:int系列、float系列、bool、string、数组和结构体

引用类型有:指针、slice切片、管道channel、接口interface、map、函数等

判断语句

基本的使用方式就不再介绍了,这里主要介绍不一样的部分

Go 的 if 有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示:

package main

import "fmt"
func main() {
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}

还有这个 switch 语句,不同的 case 之间不使用 break 分隔,默认只会执行一个 case。

switch{
case 1,2,3,4:
default:
}

如果想要执行多个 case,需要使用 fallthrough 关键字,也可用 break 终止。

switch{
case 1:
...
if(...){
break
}

fallthrough
// 此时switch(1)会执行case1和case2,但是如果满足if条件,则只执行case1

case 2:
...
case 3:
}

定义函数

定义函数和 TypeScript 有点类似,主要有个多返回值需要注意

单返回值的写法:

// 如果没有返回值可以不写
func testFunc(i int) int {
return 100 * i
}

多返回值的写法(匿名的写法):

func foo2(a int, b int) (int, int) {
return a * b, a + b
}

func main() {
ret1, ret2 := foo2(1, 2)
fmt.Println(ret1, ret2)
}

也可以使用不定长参数

func add(args ... int) (int, error){
var sum int
if len(args) == 0 {
return 0, errors.New("参数为空")
}
for _ ,arg := range args {
sum += arg
}
return sum, nil
}

具名返回值

可以给一个函数的返回值指定名字。如果指定了一个返回值的名字,则可以视为在该函数的第一行中定义了该名字的变量。

func foo3(a int, b int) (r1 int,r2 int) {
r1 = a * 100
r2 = b * 120
return
}

func main() {
ret1, ret2 := foo3(1, 2)
fmt.Println(ret1, ret2)
}

让我们写一个函数 rectProps,它接受一个矩形的长和宽,并返回该矩形的面积和周长。

package main

import (
"fmt"
)

func rectProps(length, width float64) (area, perimeter float64) {
area = length * width
perimeter = (length + width) * 2
return //no explicit return value
}

func main() {
area, perimeter := rectProps(10.8, 5.6)
fmt.Printf("Area %f Perimeter %f", area, perimeter)
}

在上面的函数中,area 和 perimeter 是命名返回值。注意 return 语句没有指定任何返回值。

因为在函数声明时已经指定 area 和 perimeter 是返回值,在遇到 return 语句时它们会自动从函数中返回。

在 Golang 中,有返回值的函数,无论是命名返回值还是普通形式的返回值,函数中必须包含 return 语句。

三个点(...)用法

第一种用法就是上面的不定长参数

第二个用法是 slice 可以被打散进行传递。

func test1(args ...string) { //可以接受任意个string参数
for _, v:= range args{
fmt.Println(v)
}
}

func main(){
var strss= []string{
"qwr",
"234",
"yui",
"cvbc",
}

test1(strss...) //切片被打散传入
}

也可以利用这个打散传递的用法合并 slice

var strss = []string{
"qwr",
"234",
"yui",
}

var strss2 = []string{
"qqq",
"aaa",
"zzz",
"zzz",
}

strss = append(strss, strss2...) //strss2的元素被打散一个个append进strss
fmt.Println(strss)

指针的使用

和 C 的指针是一样的,这里简单介绍下

值传递和 Java 是一样的

import "fmt" 

func changeValue(p int) {
p = 10
}

func main() {
a := 1
changeValue(a)
fmt.Println(a) // a = 1
}

改成使用指针的方式

func changeValue(p *int) {
*p = 10
}

func main() {
a := 1
changeValue(&a)
fmt.Println(a) // a = 10
}

写一个交换两个变量值的函数

func swap(p1 *int, p2 *int) {
temp := *p1
*p1 = *p2
*p2 = temp
}

func main() {
a := 100
b := 200
swap(&a, &b)
fmt.Println(a, b)
}

二级指针,指针的指针

func main() {
a := 100

p1 := &a
var pp **int = &p1 // 指针的地址

fmt.Println(p1, pp) // 0xc0000aa058 0xc0000ce018
}

Go 也有空指针,即 nil

package main

import "fmt"

func main() {
var ptr *int

fmt.Printf("ptr 的值为 : %x\n", ptr) // ptr 的值为 : 0
}

空指针判断:

if(ptr != nil)     /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */

Golang 的类型比较

Golang 不像 Java 那样专门提供了一个 equality 方法来比较对象

go 中不同类型是不能比较的,例如 int 不能和 string 进行比较;false 不能和 0 进行比较。

type a struct { // 它的变量 可以 == != 比较
name string
age int
}

type b struct { // 它的变量不可以 == != 比较
age int
m map[string]string
}

结构体只能比较是否相等,但是不能比较大小。

相同类型的结构体才能够进行比较,结构体是否相同不但与属性类型有关,还与属性顺序相关

如果 struct 的所有成员都可以比较,则该 struct 就可以通过 == 或 != 进行比较是否相等,比较时逐个项进行比较,如果每一项都相等,则两个结构体才相等,否则不相等;

注意,数组类型比较,例如 [2]int[3]int 类型不同,不能进行比较。 切片不能进行比较。

更多细节参考 两个值是如何进行比较的?

Reference

Golang:函数命名返回值 Golang Document